﻿// Module:             Games Programming
// Assignment1:        Platformer Game
// Add game feature:   Player
// Student Number:     S10096666C
// Student Name:       Trinh Quang Anh

//////////////////////////////////////////////////////////////////////////

using System;
using Game.Platforms;
using Game.TrapScripts;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Audio;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;

namespace Game
{
    public class Player
    {
        #region Private variables
        private readonly Level _level;

        private Animation _idleAnim, _runAnim, _jumpAnim, _dieAnim, _celebrateAnim;
        private SpriteEffects _flip = SpriteEffects.None;
        private AnimationPlayer _animPlayer;

        private bool _isAlive;
        private Vector2 _position;
        private Vector2 _velocity;
        private Rectangle _localBounds;
        private SoundEffect _killedSound, _jumpSound, _fallSound;

        // Constants for controlling horizontal movement
        private const float MoveAcceleration = 13000.0f;
        private const float MaxMoveSpeed = 5000.0f;
        private const float GroundDragFactor = 0.5f;
        private const float AirDragFactor = 0.58f;

        // Constants for controlling vertical movement
        private const float MaxJumpTime = 0.35f;
        private const float JumpLaunchVelocity = -3500.0f;
        private const float GravityAcceleration = 3400.0f;
        private const float MaxFallSpeed = 550.0f;
        private const float JumpControlPower = 0.14f;

        private bool _isOnGround;

        /// <summary>
        /// Current user movement input.
        /// </summary>
        private float _movement;

        // Jumping state
        private bool _isJumping;
        private bool _wasJumping;
        private float _jumpTime;
        private float _previousBottom;
        private SoundEffect _dieSound;

        #endregion

        #region Properties
        public bool IsOnGround
        {
            get { return _isOnGround; }
        }



        public bool IsAlive
        {
            get { return _isAlive; }
        }

        public Vector2 Position
        {
            get { return _position; }
        }

        public Rectangle BoundingRectangle
        {
            get
            {
                int left = (int)Math.Round(Position.X - _animPlayer.Origin.X) + _localBounds.X;
                int top = (int)Math.Round(Position.Y - _animPlayer.Origin.Y) + _localBounds.Y;

                return new Rectangle(left, top, _localBounds.Width, _localBounds.Height);
            }
        }

        public Vector2 Velocity
        {
            get { return _velocity; }
        }

        #endregion

        public Player(Level level)
        {
            _level = level;

            _animPlayer = new AnimationPlayer();

            LoadContent();

            Reset(Vector2.Zero);
        }

        private void LoadContent()
        {
            // Load animated textures.
            _idleAnim = new Animation(_level.Content.Load<Texture2D>("Sprites/Player/Idle"), 0.1f, true);
            _runAnim = new Animation(_level.Content.Load<Texture2D>("Sprites/Player/Run"), 0.1f, true);
            _jumpAnim = new Animation(_level.Content.Load<Texture2D>("Sprites/Player/Jump"), 0.1f, false);
            _dieAnim = new Animation(_level.Content.Load<Texture2D>("Sprites/Player/Die"), 0.1f, false);
            _celebrateAnim = new Animation(_level.Content.Load<Texture2D>("Sprites/Player/Celebrate"), 0.1f, false);

            _dieSound = _level.Content.Load<SoundEffect>("Sound Effect/death");
            _jumpSound = _level.Content.Load<SoundEffect>("Sound Effect/jump");
            

            // Calculate bounds within texture size.            
            int width = (int)(_idleAnim.FrameWidth * 0.4);
            int left = (_idleAnim.FrameWidth - width) / 2;
            int height = (int)(_idleAnim.FrameWidth * 0.8);
            int top = _idleAnim.FrameHeight - height;
            _localBounds = new Rectangle(left, top, width, height);
        }

        public void Reset(Vector2 position)
        {
            _position = position;
            _velocity = Vector2.Zero;
            _isAlive = true;
            _animPlayer.PlayAnimation(_idleAnim);
        }


        #region Update player
        public void Update(GameTime gameTime, KeyboardState keyboardState, MouseState mouseState)
        {
            GetInput(keyboardState, mouseState);

            ApplyPhysics(gameTime);

            if (IsAlive && IsOnGround)
            {
                if (Math.Abs(Velocity.X) - 0.02f > 0)
                {
                    _animPlayer.PlayAnimation(_runAnim);
                }
                else
                {
                    _animPlayer.PlayAnimation(_idleAnim);
                }
            }

            // Clear input.
            _movement = 0.0f;
            _isJumping = false;
        }

        private void GetInput(KeyboardState keyboardState, MouseState mouseState)
        {
            // If any digital horizontal movement input is found, override the analog movement.
            if (keyboardState.IsKeyDown(Keys.Left) || keyboardState.IsKeyDown(Keys.A))
            {
                _movement = -1.0f;
            }
            else if (keyboardState.IsKeyDown(Keys.Right) || keyboardState.IsKeyDown(Keys.D))
            {
                _movement = 1.0f;
            }

            // Check if the player wants to jump.
            _isJumping = keyboardState.IsKeyDown(Keys.Up) || keyboardState.IsKeyDown(Keys.W) || keyboardState.IsKeyDown(Keys.Space);
        }

        public void ApplyPhysics(GameTime gameTime)
        {
            var elapsed = (float)gameTime.ElapsedGameTime.TotalSeconds;

            Vector2 previousPosition = Position;

            // Base velocity is a combination of horizontal movement control and
            // acceleration downward due to gravity.
            _velocity.X += _movement * MoveAcceleration * elapsed;
            _velocity.Y = MathHelper.Clamp(_velocity.Y + GravityAcceleration * elapsed, -MaxFallSpeed, MaxFallSpeed);

            _velocity.Y = DoJump(_velocity.Y, gameTime);

            // Apply pseudo-drag horizontally.
            if (IsOnGround)
                _velocity.X *= GroundDragFactor;
            else
                _velocity.X *= AirDragFactor;

            // Prevent the player from running faster than his top speed.            
            _velocity.X = MathHelper.Clamp(_velocity.X, -MaxMoveSpeed, MaxMoveSpeed);

            // Apply velocity.
            _position += _velocity * elapsed;

            _position = new Vector2((float)Math.Round(Position.X), (float)Math.Round(Position.Y));

            // If the player is now colliding with the level, separate them.
            HandleCollisions();

            // If the collision stopped us from moving, reset the velocity to zero.
            if (Position.X == previousPosition.X)
                _velocity.X = 0;

            if (Position.Y == previousPosition.Y)
                _velocity.Y = 0;

            // move camera.
            // TODO: make it move nicely according to this http://www.imake-games.com/cameras-in-2d-platformers/
            //Vector2 camDisplacement = _velocity * elapsed;
            //camDisplacement.Y = 0;
            _level.Camera.SetPosition(_position);
        }

        // TODO XXX FIXME: This code is ugly and stupid, result of stupid physics code
        // Absolutely unacceptable and must be fixed ASAP
        // Use a proper physics engine for this
        public void HandleCollisions()
        {
            var bounds = BoundingRectangle;

            _isOnGround = false;

            foreach (var platform in _level.Platforms)
            {
                Vector2 depth = bounds.GetIntersectionDepth(platform.BoundingRect);
                if (depth != Vector2.Zero)
                {
                    float absDepthX = Math.Abs(depth.X);
                    float absDepthY = Math.Abs(depth.Y);

                    switch (platform.Type)
                    {
                            /*
                        case PlatformType.Exit:
                            _level.OnPlayerReachedExit();
                            _animPlayer.PlayAnimation(_celebrateAnim);
                            _reachedExit = true;
                            goto default;
                             */
                        case PlatformType.Disappearing:
                            ((DisappearingPlatform)platform).Activate();
                            goto default;
                        case PlatformType.Elevator:
                            _isOnGround = true;
                            _position.Y = platform.Position.Y - depth.Y;
                            goto default;
                        default:
                            if (absDepthY < absDepthX)
                            {
                                // If we crossed the top of a tile, we are on the ground.
                                if (_previousBottom <= platform.BoundingRect.Top)
                                    _isOnGround = true;

                                // Ignore platforms, unless we are on the ground.

                                if (IsOnGround)
                                {
                                    // Resolve the collision along the Y axis.
                                    _position = new Vector2(Position.X, Position.Y + depth.Y);

                                    // Perform further collisions with the new bounds.
                                    bounds = BoundingRectangle;
                                }

                            }
                            else // Ignore platforms.
                            {
                                // Resolve the collision along the X axis.

                                _position = new Vector2(Position.X + depth.X, Position.Y);

                                // Perform further collisions with the new bounds.
                                bounds = BoundingRectangle;
                            }
                            break;
                    }




                }
            }

            foreach (var enemy in _level.Enemies)
            {
                var depth = bounds.GetIntersectionDepth(enemy.GetBoundingRect());
                if (depth != Vector2.Zero)
                {
                    OnPlayerKilled();
                    return;
                }
            }

            foreach (var trap in _level.Traps)
            {
                var depth = bounds.GetIntersectionDepth(trap.GetBoundingRect());
                if (depth != Vector2.Zero)
                {
                    switch (trap.Type)
                    {
                        case TrapType.LaserDoor:
                            if (trap.IsActive)
                                OnPlayerKilled();
                            break;
                        default:
                            OnPlayerKilled();
                            break;
                    }
                }
            }

            _previousBottom = bounds.Bottom;
        }

        // TODO: Should eliminate double jumping, depends on gameplay
        private float DoJump(float velocityY, GameTime gameTime)
        {
            // If the player wants to jump
            if (_isJumping)
            {
                // Begin or continue a jump
                if ((!_wasJumping && IsOnGround) || _jumpTime > 0.0f)
                {
                    if (_jumpTime == 0.0f)
                    {
                        //jumpSound.Play();
                        _jumpSound.Play();
                    }

                    _jumpTime += (float)gameTime.ElapsedGameTime.TotalSeconds;
                    _animPlayer.PlayAnimation(_jumpAnim);
                }

                // If we are in the ascent of the jump
                if (0.0f < _jumpTime && _jumpTime <= MaxJumpTime)
                {
                    // Fully override the vertical velocity with a power curve that gives players more control over the top of the jump
                    velocityY = JumpLaunchVelocity * (1.0f - (float)Math.Pow(_jumpTime / MaxJumpTime, JumpControlPower));
                }
                else
                {
                    // Reached the apex of the jump
                    _jumpTime = 0.0f;
                }
            }
            else
            {
                // Continues not jumping or cancels a jump in progress
                _jumpTime = 0.0f;
            }
            _wasJumping = _isJumping;

            return velocityY;
        }

        #endregion

        public void OnPlayerKilled()
        {
            _animPlayer.PlayAnimation(_dieAnim);
            _dieSound.Play();
            _isAlive = false;
        }

        public void OnReachedExit()
        {
            Console.WriteLine("Player has reached Exit");
            _animPlayer.PlayAnimation(_celebrateAnim);
        }

        public void Draw(GameTime gameTime, SpriteBatch spriteBatch)
        {
            // Flip the sprite to face the way we are moving.
            if (Velocity.X > 0)
                _flip = SpriteEffects.FlipHorizontally;
            else if (Velocity.X < 0)
                _flip = SpriteEffects.None;

            // Draw that sprite.
            _animPlayer.Draw(gameTime, spriteBatch, Position, _flip);
        }
    }
}
